﻿using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq; 
using System.Text;

namespace etcetera.Mvc.UI.Calendar
{
    public class Calendar : ICalendar
    {
        private int _month;
        private int _year;
        private string _id;
        private DayOfWeek _firstDayInWeek = DayOfWeek.Monday;
        private bool _showPrevNextMonth;
        private bool _highlightToday;
        private IEnumerable<ICalendarEntry> _entries = new List<ICalendarEntry>();
        private string _dayUrl;
        private bool _hideEntries;
        private IDictionary<string, object> _attributes = new Dictionary<string, object>();
        private CultureInfo _cultureInfo = CultureInfo.CurrentCulture;

        /// <summary>
        /// Initializes a new instance of the <see cref="Calendar"/> class.
        /// </summary>
        /// <param name="year">The year.</param>
        /// <param name="month">The month.</param>
        public Calendar(int year, int month)
        {
            _year = year;
            _month = month;
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="Calendar"/> class.
        /// </summary>
        /// <param name="year">The year.</param>
        /// <param name="month">The month.</param>
        /// <param name="htmlAttributes">The HTML attributes.</param>
        public Calendar(int year, int month, IDictionary<string, object> htmlAttributes)
        {
            _year = year;
            _month = month;
            _attributes = htmlAttributes;    
        }

        public ICalendar WithId(string id)
        {
            _id = id;
            return this;
        }

        public ICalendar FirstDayInWeek(DayOfWeek day)
        {
            _firstDayInWeek = day;
            return this;
        }

        public ICalendar ShowPrevNextMonth()
        {
            _showPrevNextMonth = true;
            return this;
        }

        public ICalendar HighlightToday()
        {
            _highlightToday = true;
            return this;
        }

        public ICalendar Culture(string culture)
        {
            _cultureInfo = CultureInfo.GetCultureInfo(culture);
            return this;
        }

        public ICalendar Entries(IEnumerable<ICalendarEntry> entries)
        {
            _entries = entries;
            return this;
        }

        public ICalendar DayUrl(string url)
        {
            _dayUrl = url;
            return this;
        }

        public ICalendar HideEntries()
        {
            _hideEntries = true;
            return this;
        }

        private int GetNumberOfDays()
        {
            return DateTime.DaysInMonth(_year, _month);
        }

        private int GetPrevMonthNumberOfDays()
        {
            DateTime dt = new DateTime(_year, _month, 1);
            dt = dt.AddMonths(-1);

            return DateTime.DaysInMonth(dt.Year, dt.Month);
        }

        private int GetNextMonthNumberOfDays()
        {
            DateTime dt = new DateTime(_year, _month, 1);
            dt = dt.AddMonths(1);
            
            return DateTime.DaysInMonth(dt.Year, dt.Month);
        }

        /// <summary>
        /// Gets the first weekday in the month. Monday is 1, Sunday = 7.
        /// </summary>
        /// <returns></returns>
        private int GetFirstDay()
        {
            DateTime dtFirst = new DateTime(_year, _month, 1);
            
            int firstDay = (int)dtFirst.DayOfWeek; // Sunday = 0, Saturday = 6
            if (firstDay == 0) 
                firstDay = 7;
            
            return firstDay;
        }

        /// <summary>
        /// Gets the number of weeks in this month.
        /// </summary>
        /// <returns></returns>
        private int GetNumberOfWeeks()
        {
            int numberOfDays = GetNumberOfDays();

            int firstDay = GetFirstMonthDayCellNumber();

            int fields = firstDay + numberOfDays - 1;
            return (int)Math.Ceiling(fields / 7.0f);
        }

        /// <summary>
        /// Gets the cell number of the first day in the month considering the starting week day.
        /// </summary>
        /// <returns></returns>
        private int GetFirstMonthDayCellNumber()
        {
            int firstDay = GetFirstDay();
            int firstDayInWeek = (int) _firstDayInWeek;
            //if (firstDayInWeek == 0)
            //    firstDayInWeek = 7;

            if (firstDay >= firstDayInWeek)
                firstDay = firstDay - firstDayInWeek + 1;
            else
                firstDay = firstDay + firstDayInWeek - 2 * (firstDayInWeek - firstDay - 1);
            return firstDay;
        }

        /// <summary>
        /// Gets the first day to display of the previous month.
        /// </summary>
        /// <returns></returns>
        private int GetFirstDayOfPreviousMonth()
        {
            DateTime prevMonth = new DateTime(_year, _month, 1).AddMonths(-1);
            int prevMonthTotalDays = DateTime.DaysInMonth(prevMonth.Year, prevMonth.Month);

            int firstDay = GetFirstMonthDayCellNumber();

            return prevMonthTotalDays - firstDay + 2;
        }

        /// <summary>
        /// Converts the specified attributes dictionary of key-value pairs into a string of HTML attributes. 
        /// </summary>
        /// <returns></returns>
        private static string BuildHtmlAttributes(IDictionary<string, object> attributes)
        {
            if (attributes == null || attributes.Count == 0)
            {
                return string.Empty;
            }

            const string attributeFormat = "{0}=\"{1}\"";

            return string.Join(" ", attributes.Select(pair => string.Format(attributeFormat, pair.Key, pair.Value)).ToArray());
        }

        /// <summary>
        /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();

            string id = string.IsNullOrEmpty(_id) ? string.Empty : string.Format(" id='{0}'", _id);

            string attrs = BuildHtmlAttributes(_attributes);

            if (_attributes.ContainsKey("class"))
            {
                // Merge the CSS class names
                sb.AppendFormat("<div class='{0} {1}'{2}>", "calendar", _attributes["class"], id);
            }
            else
            {
                if (attrs.Length > 0)
                    sb.AppendFormat("<div class='{0}'{1} {2}>", "calendar", id, attrs);
                else
                    sb.AppendFormat("<div class='{0}'{1}>", "calendar", id);
            }

            sb.AppendLine("<table>");
            
            int weeks = GetNumberOfWeeks();
            int days = GetNumberOfDays();
            
            // Calculate the cell number of the first day in the month considering the starting week day.
            int firstDay = GetFirstMonthDayCellNumber();

            int currentDay = 1;
            
            // Table row with day names
            sb.AppendLine("<thead>");
            sb.AppendLine("<tr>");
            for (int d = 0; d < 7; d++)
            {
                int dayNameVal = (d + (int)_firstDayInWeek) % 7;
                string dayName = string.Empty;
                // Fall back to the system culture in case an invalid culture was specified
                try
                {
                    dayName = _cultureInfo.DateTimeFormat.DayNames[dayNameVal].Substring(0, 2);
                }
                catch
                {
                    dayName = CultureInfo.CurrentCulture.DateTimeFormat.DayNames[dayNameVal].Substring(0, 2);
                }
                sb.AppendFormat("<th>{0}</th>", dayName);
            }
            sb.AppendLine("</tr>");
            sb.AppendLine("</thead>");
            
            sb.AppendLine("<tbody>");

            int prevMonthDay = GetFirstDayOfPreviousMonth();
            int nextMonthDay = 1;

            string entriesFormat = "<div class='entry'>{0}</div>";
            string entryFormat = "<p><a href='{0}'>{1}</a></p>";

            // One row per week
            for (int w = 0; w < weeks; w++)
            {
                sb.AppendLine("<tr>");
                // One cell per day
                for (int d = 0; d < 7; d++)
                {
                    // Apply a CSS class if the day is a Saturday or a Sunday.
                    bool isWeekend = false;
                    string weekendClass = string.Empty;
                    DayOfWeek currDay = (DayOfWeek) ((d + (int) _firstDayInWeek) % 7);
                    if (currDay == DayOfWeek.Saturday || currDay == DayOfWeek.Sunday)
                    {
                        weekendClass = " class='wend'";
                        isWeekend = true;
                    }

                    // Apply a "today" class
                    DateTime dt = new DateTime(_year, _month, 1);
                    dt = dt.AddDays(currentDay - 1).Date;
                    if (_highlightToday && DateTime.Now.Date == dt)
                        weekendClass = weekendClass == string.Empty ? " class='today'" : " class='wend today'";
                    
                    // In the first week, start with empty day table cells until the first of the month.
                    if (w == 0 && firstDay > d + 1)
                    {
                        if (_showPrevNextMonth)
                        {
                            string dayUrlFormat = "{0}";
                            if (!string.IsNullOrEmpty(_dayUrl))
                            {
                                string dayUrl = GetDayUrl(prevMonthDay - GetPrevMonthNumberOfDays() - 1);
                                dayUrlFormat = "<a href=\"{0}\">{1}</a>";
                                dayUrlFormat = string.Format(dayUrlFormat, dayUrl, prevMonthDay);
                            }
                            else
                                dayUrlFormat = string.Format(dayUrlFormat, prevMonthDay);

                            if (_hideEntries)
                                sb.AppendFormat("<td valign='top' class='dis'><div class='day'>{0}</div></td>", dayUrlFormat);
                            else
                                sb.AppendFormat("<td valign='top' class='dis'><div class='day'>{0}</div><div class='entry'>&nbsp;</div></td>", dayUrlFormat);
                            
                            prevMonthDay++;
                        }
                        else
                            sb.Append("<td valign='top' class='dis'>&nbsp;</td>");
                    }
                    else if (currentDay <= days)
                    {
                        DateTime dtCurrent = new DateTime(_year, _month, currentDay).Date;

                        string entries = "&nbsp;";

                        var calendarEntries = from entry in _entries where entry.Date.Date == dtCurrent select entry;

                        if (calendarEntries.Count() > 0)
                        {
                            entries = string.Empty;
                            foreach (var calendarEntry in calendarEntries)
                                entries += string.Format(entryFormat, calendarEntry.Url, calendarEntry.Title);
                        }

                        string entryDiv = string.Format(entriesFormat, entries);

                        string dayUrlFormat = "{0}";
                        if (!string.IsNullOrEmpty(_dayUrl))
                        {
                            string dayUrl = GetDayUrl(currentDay - 1);
                            dayUrlFormat = "<a href=\"{0}\">{1}</a>";
                            dayUrlFormat = string.Format(dayUrlFormat, dayUrl, currentDay);
                        }
                        else
                            dayUrlFormat = string.Format(dayUrlFormat, currentDay);

                        if (_hideEntries)
                            sb.AppendFormat("<td valign='top'{1}><div class='day'>{0}</div></td>", dayUrlFormat, weekendClass);
                        else
                            sb.AppendFormat("<td valign='top'{1}><div class='day'>{0}</div>{2}</td>", dayUrlFormat, weekendClass, entryDiv);
                            
                        currentDay++;
                    }
                    else
                    {
                        string cls = "class='dis'";
                        if (isWeekend)
                            cls = "class='wend dis'";
                        
                        if (_showPrevNextMonth)
                        {
                            string dayUrlFormat = "{0}";
                            if (!string.IsNullOrEmpty(_dayUrl))
                            {
                                string dayUrl = GetDayUrl(nextMonthDay + GetNumberOfDays() - 1);
                                dayUrlFormat = "<a href=\"{0}\">{1}</a>";
                                dayUrlFormat = string.Format(dayUrlFormat, dayUrl, nextMonthDay);
                            }
                            else
                                dayUrlFormat = string.Format(dayUrlFormat, nextMonthDay);

                            if (_hideEntries)
                                sb.AppendFormat("<td valign='top' {1}><div class='day'>{0}</div></td>", dayUrlFormat, cls);
                            else
                                sb.AppendFormat("<td valign='top' {1}><div class='day'>{0}</div><div class='entry'>&nbsp;</div></td>", dayUrlFormat, cls);

                            nextMonthDay++;
                        }
                        else
                            sb.Append(string.Format("<td valign='top' {0}>&nbsp;</td>", cls));
                    }
                }
                sb.AppendLine("</tr>");
            }
            
            sb.AppendLine("</tbody>");
            sb.AppendLine("</table>");
            sb.AppendLine("</div>");
            
            return sb.ToString();
        }

        private string GetDayUrl(int dayOffset)
        {
            string dayUrl = _dayUrl;

            // Compose a valid date
            DateTime dt = new DateTime(_year, _month, 1);
            dt = dt.AddDays(dayOffset);

            dayUrl = string.Format(dayUrl, dt.Year, dt.Month, dt.Day);

            return dayUrl;
        }
    }
}
